Kuasai autentikasi OAuth2 FastAPI! Panduan ini mencakup alur kata sandi, alur implisit, alur kode otorisasi, penyegaran token, dan praktik terbaik keamanan untuk membangun API yang kuat.
FastAPI OAuth2 Implementation: A Comprehensive Authentication Flow Guide
Di lanskap digital saat ini, mengamankan API Anda sangat penting. OAuth2 (Open Authorization) telah menjadi standar industri untuk otorisasi yang didelegasikan, memungkinkan pengguna untuk memberikan akses terbatas ke sumber daya mereka tanpa membagikan kredensial mereka. FastAPI, sebuah framework web Python modern dan berkinerja tinggi, membuat implementasi autentikasi OAuth2 menjadi mudah. Panduan komprehensif ini akan memandu Anda melalui berbagai alur OAuth2 dan menunjukkan cara mengintegrasikannya ke dalam aplikasi FastAPI Anda, memastikan API Anda tetap aman dan mudah diakses.
Understanding OAuth2 Concepts
Sebelum masuk ke kode, mari kita buat pemahaman yang jelas tentang konsep inti OAuth2:
- Resource Owner: Pengguna yang memiliki data dan memberikan akses.
- Client: Aplikasi yang meminta akses ke data pemilik sumber daya. Ini bisa berupa aplikasi web, aplikasi seluler, atau layanan lainnya.
- Authorization Server: Mengautentikasi pemilik sumber daya dan memberikan otorisasi ke klien.
- Resource Server: Menampung sumber daya yang dilindungi dan memverifikasi token akses sebelum memberikan akses.
- Access Token: Kredensial yang mewakili otorisasi yang diberikan oleh pemilik sumber daya kepada klien.
- Refresh Token: Kredensial berumur panjang yang digunakan untuk mendapatkan token akses baru tanpa mengharuskan pemilik sumber daya untuk mengotorisasi ulang.
- Scopes: Mendefinisikan izin spesifik yang diminta oleh klien.
OAuth2 Flows: Choosing the Right Approach
OAuth2 mendefinisikan beberapa alur otorisasi, masing-masing cocok untuk skenario yang berbeda. Berikut adalah rincian dari alur yang paling umum dan kapan menggunakannya:
1. Password (Resource Owner Password Credentials) Flow
Description: Klien langsung mendapatkan token akses dari server otorisasi dengan memberikan nama pengguna dan kata sandi pemilik sumber daya. Use Case: Aplikasi yang sangat tepercaya, seperti aplikasi seluler pihak pertama. Ini hanya boleh digunakan ketika alur lain tidak layak. Pros: Mudah diimplementasikan. Cons: Mengharuskan klien untuk menangani kredensial pemilik sumber daya, meningkatkan risiko paparan jika klien disusupi. Kurang aman daripada alur lainnya. Example: Aplikasi seluler perusahaan sendiri yang mengakses API internal mereka.
Implementation in FastAPI:
First, install the necessary packages:
pip install fastapi uvicorn python-multipart passlib[bcrypt] python-jose[cryptography]
Now, let's create a basic example:
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from datetime import datetime, timedelta
app = FastAPI()
# Replace with a strong, randomly generated secret key
SECRET_KEY = "YOUR_SECRET_KEY"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
# Password hashing configuration
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# Dummy user database (replace with a real database in production)
users = {
"johndoe": {
"username": "johndoe",
"hashed_password": pwd_context.hash("password123"),
"scopes": ["read", "write"]
}
}
# Function to verify password
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
# Function to create access token
def create_access_token(data: dict, expires_delta: timedelta):
to_encode = data.copy()
expire = datetime.utcnow() + expires_delta
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
# OAuth2 endpoint for token generation
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
user = users.get(form_data.username)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
if not verify_password(form_data.password, user["hashed_password"]):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user["username"], "scopes": user["scopes"]},
expires_delta=access_token_expires,
)
return {"access_token": access_token, "token_type": "bearer"}
# Dependency to authenticate requests
async def get_current_user(token: str):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = users.get(username)
if user is None:
raise credentials_exception
return user
async def get_current_active_user(current_user = Depends(get_current_user)):
return current_user
# Example protected endpoint
@app.get("/users/me")
async def read_users_me(current_user = Depends(get_current_active_user)):
return {"username": current_user["username"], "scopes": current_user["scopes"]}
Explanation:
- Dependencies: We use `fastapi.security.OAuth2PasswordRequestForm` for handling the username and password.
- Password Hashing: `passlib` is used for securely hashing and verifying passwords. Never store passwords in plain text!
- JWT Generation: `python-jose` is used for creating and verifying JSON Web Tokens (JWTs).
- `/token` endpoint: This endpoint handles the login process. It validates the username and password, and if valid, generates an access token.
- `get_current_user` dependency: This function verifies the access token and retrieves the user.
- `/users/me` endpoint: This is a protected endpoint that requires a valid access token to access.
2. Implicit Flow
Description: Klien langsung menerima token akses dari server otorisasi setelah pemilik sumber daya mengautentikasi. Token akses dikembalikan dalam fragmen URL. Use Case: Aplikasi halaman tunggal (SPA) dan aplikasi berbasis browser lainnya di mana menyimpan rahasia klien tidak layak. Pros: Sederhana untuk aplikasi berbasis browser. Cons: Kurang aman daripada alur lainnya karena token akses diekspos di URL. Tidak ada token refresh yang diterbitkan. Example: Aplikasi JavaScript yang mengakses API media sosial.
Implementation Considerations in FastAPI:
While FastAPI doesn't directly handle the frontend aspects of the Implicit Flow (as it's primarily a backend framework), you would use a frontend framework like React, Vue, or Angular to manage the authentication flow. FastAPI would primarily act as the Resource Server.
Simplified Backend (FastAPI - Resource Server) Example:
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2AuthorizationCodeBearer
from jose import JWTError, jwt
app = FastAPI()
# Replace with a strong, randomly generated secret key
SECRET_KEY = "YOUR_SECRET_KEY"
ALGORITHM = "HS256"
# Dummy user database (replace with a real database in production)
users = {
"johndoe": {
"username": "johndoe",
"scopes": ["read", "write"]
}
}
# OAuth2 scheme - using AuthorizationCodeBearer for token verification
oauth2_scheme = OAuth2AuthorizationCodeBearer(authorizationUrl="/auth", tokenUrl="/token") # These URLs are handled by the Authorization Server (not this FastAPI app).
# Dependency to authenticate requests
async def get_current_user(token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = users.get(username)
if user is None:
raise credentials_exception
return user
async def get_current_active_user(current_user = Depends(get_current_user)):
return current_user
# Example protected endpoint
@app.get("/users/me")
async def read_users_me(current_user = Depends(get_current_active_user)):
return {"username": current_user["username"], "scopes": current_user["scopes"]}
Key Points for Implicit Flow with FastAPI:
- Authorization Server's Role: The actual authorization and token issuance happens on a separate Authorization Server. FastAPI acts as the Resource Server, validating the token.
- Frontend Handling: The frontend application (e.g., React, Vue) handles the redirect to the Authorization Server, the user login, and the retrieval of the access token from the URL fragment.
- Security Considerations: Due to the exposure of the access token in the URL, it's crucial to use HTTPS and keep the token lifetime short. The implicit flow should be avoided if possible in favor of the Authorization Code Flow with PKCE.
3. Authorization Code Flow
Description: Klien pertama-tama mendapatkan kode otorisasi dari server otorisasi, yang kemudian ditukarkan dengan token akses. Alur ini melibatkan pengalihan dari klien ke server otorisasi dan kembali. Use Case: Aplikasi web dan aplikasi seluler di mana rahasia klien dapat disimpan dengan aman. Pros: Lebih aman daripada Implicit Flow karena token akses tidak diekspos secara langsung di browser. Cons: Lebih kompleks untuk diimplementasikan daripada Implicit Flow. Example: Aplikasi pihak ketiga yang meminta akses ke data Google Drive pengguna.
Authorization Code Flow with PKCE (Proof Key for Code Exchange):
PKCE adalah ekstensi untuk Authorization Code Flow yang mengurangi risiko intersepsi kode otorisasi. Sangat direkomendasikan untuk aplikasi seluler dan SPA, karena tidak mengharuskan klien untuk menyimpan rahasia.
Implementation Considerations in FastAPI: Similar to the Implicit Flow, FastAPI would primarily act as the Resource Server in this flow. A separate Authorization Server is responsible for the authentication and authorization code issuance.
Simplified Backend (FastAPI - Resource Server) Example (Similar to Implicit Flow):
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2AuthorizationCodeBearer
from jose import JWTError, jwt
app = FastAPI()
# Replace with a strong, randomly generated secret key
SECRET_KEY = "YOUR_SECRET_KEY"
ALGORITHM = "HS256"
# Dummy user database (replace with a real database in production)
users = {
"johndoe": {
"username": "johndoe",
"scopes": ["read", "write"]
}
}
# OAuth2 scheme - using AuthorizationCodeBearer for token verification
oauth2_scheme = OAuth2AuthorizationCodeBearer(authorizationUrl="/auth", tokenUrl="/token") # These URLs are handled by the Authorization Server.
# Dependency to authenticate requests
async def get_current_user(token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = users.get(username)
if user is None:
raise credentials_exception
return user
async def get_current_active_user(current_user = Depends(get_current_user)):
return current_user
# Example protected endpoint
@app.get("/users/me")
async def read_users_me(current_user = Depends(get_current_active_user)):
return {"username": current_user["username"], "scopes": current_user["scopes"]}
Key Points for Authorization Code Flow with PKCE with FastAPI:
- Authorization Server's Role: The Authorization Server handles the generation of the authorization code, verification of the PKCE code verifier, and issuance of the access token.
- Frontend Handling: The frontend application generates a code verifier and code challenge, redirects the user to the Authorization Server, receives the authorization code, and exchanges it for an access token.
- Increased Security: PKCE prevents authorization code interception attacks, making it suitable for SPAs and mobile apps.
- Recommended Approach: The Authorization Code Flow with PKCE is generally the most secure and recommended flow for modern web and mobile applications.
4. Client Credentials Flow
Description: Klien mengautentikasi langsung dengan server otorisasi menggunakan kredensialnya sendiri (ID klien dan rahasia klien) untuk mendapatkan token akses. Use Case: Komunikasi antar mesin, seperti layanan backend yang saling mengakses. Pros: Sederhana untuk layanan backend. Cons: Tidak cocok untuk autentikasi pengguna. Example: Layanan pemrosesan data yang mengakses layanan database.
Implementation in FastAPI:
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from jose import JWTError, jwt
from datetime import datetime, timedelta
app = FastAPI()
# Replace with a strong, randomly generated secret key
SECRET_KEY = "YOUR_SECRET_KEY"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
# Dummy client database (replace with a real database in production)
clients = {
"client_id": {
"client_secret": "client_secret",
"scopes": ["read", "write"]
}
}
# Function to create access token
def create_access_token(data: dict, expires_delta: timedelta):
to_encode = data.copy()
expire = datetime.utcnow() + expires_delta
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
# HTTP Basic Authentication scheme
security = HTTPBasic()
# Endpoint for token generation
@app.post("/token")
async def login(credentials: HTTPBasicCredentials = Depends(security)):
client = clients.get(credentials.username)
if not client:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect client ID or secret",
headers={"WWW-Authenticate": "Basic"},
)
if credentials.password != client["client_secret"]:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect client ID or secret",
headers={"WWW-Authenticate": "Basic"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": credentials.username, "scopes": client["scopes"]},
expires_delta=access_token_expires,
)
return {"access_token": access_token, "token_type": "bearer"}
# Dependency to authenticate requests
async def get_current_client(token: str):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
client_id: str = payload.get("sub")
if client_id is None:
raise credentials_exception
except JWTError:
raise credentials_exception
client = clients.get(client_id)
if client is None:
raise credentials_exception
return client
async def get_current_active_client(current_client = Depends(get_current_client)):
return current_client
# Example protected endpoint
@app.get("/data")
async def read_data(current_client = Depends(get_current_active_client)):
return {"message": "Data accessed by client: " + current_client["client_secret"]}
Explanation:
- HTTP Basic Authentication: We use `fastapi.security.HTTPBasic` for authenticating the client.
- `/token` endpoint: This endpoint handles the client authentication. It validates the client ID and secret, and if valid, generates an access token.
- `get_current_client` dependency: This function verifies the access token and retrieves the client.
- `/data` endpoint: This is a protected endpoint that requires a valid access token to access.
Token Refresh
Token akses biasanya memiliki masa pakai yang singkat untuk meminimalkan dampak dari token yang disusupi. Token refresh adalah kredensial berumur panjang yang dapat digunakan untuk mendapatkan token akses baru tanpa mengharuskan pengguna untuk mengotorisasi ulang.
Implementation Considerations:
- Storing Refresh Tokens: Refresh tokens should be stored securely, ideally encrypted in a database.
- Refresh Token Endpoint: Create a dedicated endpoint (e.g., `/refresh_token`) to handle refresh token requests.
- Revoking Refresh Tokens: Implement a mechanism to revoke refresh tokens if they are compromised or no longer needed.
Example (Extending the Password Flow Example):
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from datetime import datetime, timedelta
import secrets # For generating secure random strings
app = FastAPI()
# Replace with a strong, randomly generated secret key
SECRET_KEY = "YOUR_SECRET_KEY"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
REFRESH_TOKEN_EXPIRE_DAYS = 30 # Longer lifetime for refresh tokens
# Password hashing configuration
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# Dummy user database (replace with a real database in production)
users = {
"johndoe": {
"username": "johndoe",
"hashed_password": pwd_context.hash("password123"),
"scopes": ["read", "write"],
"refresh_token": None # Store refresh token here
}
}
# Function to verify password (same as before)
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
# Function to create access token (same as before)
def create_access_token(data: dict, expires_delta: timedelta):
to_encode = data.copy()
expire = datetime.utcnow() + expires_delta
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
# Function to create refresh token
def create_refresh_token():
return secrets.token_urlsafe(32) # Generate a secure random string
# OAuth2 endpoint for token generation
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
user = users.get(form_data.username)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
if not verify_password(form_data.password, user["hashed_password"]):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
# Create refresh token and store it (securely in a database in real-world)
refresh_token = create_refresh_token()
user["refresh_token"] = refresh_token # Store it in the user object for now (INSECURE for production)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user["username"], "scopes": user["scopes"]},
expires_delta=access_token_expires,
)
return {"access_token": access_token, "token_type": "bearer", "refresh_token": refresh_token}
# Endpoint for refreshing the access token
@app.post("/refresh_token")
async def refresh_access_token(refresh_token: str):
# Find user by refresh token (securely query the database)
user = next((user for user in users.values() if user["refresh_token"] == refresh_token), None)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid refresh token",
headers={"WWW-Authenticate": "Bearer"},
)
# Create a new access token
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user["username"], "scopes": user["scopes"]},
expires_delta=access_token_expires,
)
return {"access_token": access_token, "token_type": "bearer"}
# Dependency to authenticate requests (same as before)
async def get_current_user(token: str):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = next((user for user in users.values() if user["username"] == username), None)
if user is None:
raise credentials_exception
return user
async def get_current_active_user(current_user = Depends(get_current_user)):
return current_user
# Example protected endpoint (same as before)
@app.get("/users/me")
async def read_users_me(current_user = Depends(get_current_active_user)):
return {"username": current_user["username"], "scopes": current_user["scopes"]}
Important Security Notes:
- Storing Refresh Tokens: The example stores the refresh token in memory (insecurely). In a production environment, store refresh tokens securely in a database, preferably encrypted.
- Refresh Token Rotation: Consider implementing refresh token rotation. After a refresh token is used, generate a new refresh token and invalidate the old one. This limits the impact of compromised refresh tokens.
- Auditing: Log refresh token usage to detect suspicious activity.
Security Best Practices
Mengimplementasikan OAuth2 hanyalah langkah pertama. Mematuhi praktik terbaik keamanan sangat penting untuk melindungi API dan data pengguna Anda.
- Use HTTPS: Selalu gunakan HTTPS untuk mengenkripsi komunikasi antara klien, server otorisasi, dan server sumber daya.
- Validate Input: Validasi semua data masukan secara menyeluruh untuk mencegah serangan injeksi.
- Rate Limiting: Implementasikan pembatasan laju untuk mencegah serangan brute-force.
- Regularly Update Dependencies: Jaga agar framework FastAPI Anda dan semua dependensi tetap mutakhir untuk menambal kerentanan keamanan.
- Use Strong Secrets: Buat rahasia acak yang kuat untuk rahasia klien Anda dan kunci penandatanganan JWT. Simpan rahasia ini dengan aman (misalnya, menggunakan variabel lingkungan atau sistem manajemen rahasia).
- Monitor and Log: Pantau API Anda untuk aktivitas mencurigakan dan catat semua peristiwa autentikasi dan otorisasi.
- Enforce Least Privilege: Berikan kepada klien hanya izin (cakupan) yang diperlukan.
- Proper Error Handling: Hindari mengekspos informasi sensitif dalam pesan kesalahan.
- Consider using a well-vetted OAuth2 library: Alih-alih mengimplementasikan OAuth2 dari awal, pertimbangkan untuk menggunakan library OAuth2 yang teruji dengan baik seperti Authlib. Authlib menyediakan implementasi OAuth2 yang lebih kuat dan aman.
Beyond the Basics: Advanced Considerations
Setelah Anda memiliki implementasi OAuth2 dasar, pertimbangkan topik lanjutan ini:
- Consent Management: Berikan pengguna kendali yang jelas dan terperinci atas izin yang mereka berikan kepada klien.
- Delegated Authorization: Implementasikan dukungan untuk otorisasi yang didelegasikan, yang memungkinkan pengguna untuk mengotorisasi klien untuk bertindak atas nama mereka.
- Multi-Factor Authentication (MFA): Integrasikan MFA untuk meningkatkan keamanan.
- Federated Identity: Dukung autentikasi melalui penyedia identitas pihak ketiga (misalnya, Google, Facebook, Twitter).
- Dynamic Client Registration: Izinkan klien untuk mendaftarkan diri secara dinamis dengan server otorisasi Anda.
Conclusion
Mengimplementasikan autentikasi OAuth2 dengan FastAPI adalah cara yang ampuh untuk mengamankan API Anda dan melindungi data pengguna. Dengan memahami berbagai alur OAuth2, menerapkan praktik terbaik keamanan, dan mempertimbangkan topik lanjutan, Anda dapat membangun API yang kuat dan aman yang memenuhi kebutuhan pengguna dan aplikasi Anda. Ingatlah untuk memilih alur yang sesuai untuk kasus penggunaan spesifik Anda, prioritaskan keamanan, dan terus pantau dan tingkatkan sistem autentikasi Anda. Sementara contoh yang diberikan menunjukkan prinsip-prinsip fundamental, selalu sesuaikan dengan persyaratan khusus Anda dan konsultasikan dengan pakar keamanan untuk peninjauan menyeluruh.